在這篇文章中我們會使用到 Production Service 與 Main App,請參考第四章節所提到的內容建立你的本地開發環境。
在開始本章前我們得先調整一下範例微服務的組態設定檔案,請先定位到 Production Service 目錄下,你應該會看到一個名為 .rr.yml
的檔案,將他打開後關注第 11 行的區塊:
http:
address: "0.0.0.0:8080"
static:
dir: "/app/public"
forbid: [".htaccess", ".php"]
pool:
num_workers: 1
# max_jobs: 64
# debug: true
第 17 行顯示出目前伺服器僅有一個 worker 處理 HTTP 連線,這意味著在同一個時間內這個 Service 僅能處理一個連線請求。在此筆者建議你將 num_workers
設定為 CPU 核心數的兩倍,這會使往後的開發更加順暢。
更新完 .rr.yml
後,你需要下指令重新啟動伺服器。你可以使用 docker compose restart
或者是先 docker compose down
再 docker compose up -d
。
API 組合(Composition)模式
如同第二章提到的,開發人員可以透過 API 組合模式實現在單一請求中與多個端點(或資源)進行查詢。舉個例子,我們通常會期望傳入複數個商品 ID ,並在一個請求中查詢這些商品 ID 的詳細資訊,並一次回傳。
我們可能會寫出以下程式碼:
<?php
require_once './init.php';
use SDPMlab\Anser\Service\Action;
use Psr\Http\Message\ResponseInterface;
header("Content-Type: application/json");
$productIds = $_GET['products'] ?? [];
if (count($productIds) == 0) {
echo json_encode([
"data" => [],
"message" => "No products, please add products id in query string. like: ?products[]=1&products[]=2"
]) . PHP_EOL;
http_response_code(200);
exit;
}
$data = [];
foreach ($productIds as $productId) {
$action = (new Action(
serviceName: "ProductionService",
method: "GET",
path: "/api/v1/products/{$productId}"
))->doneHandler(static function(
ResponseInterface $response,
Action $runtimeAction
) {
$body = $response->getBody()->getContents();
$data = json_decode($body, true);
$runtimeAction->setMeaningData($data['data']);
});
$data[$productId] = $action->do()->getMeaningData();
}
echo json_encode([
"data" => $data
]) . PHP_EOL;
$_GET['products']
獲取 URL 查詢字串中的 products
參數。例如:?products[]=1&products[]=2
會得到一個包含 1 和 2 的商品 ID 陣列。productId
,創建一個新的 Action
類別實體,指定要訪問的 API 服務名稱、方法和路徑。doneHandler
設定一個匿名函式,將響應內容解析為 JSON 並存儲結果數據。do()
方法發送 API 請求,並將結果數據存儲在 $data
陣列中,使用 productId
作為 Key。$data
陣列編碼為 JSON 並返回給 Client。讓我們試試看使用 Postman 實際執行這段程式碼:
{{main_service}}/multi_action.php?products[]=1&products[]=5&products[]=42&products[]=55
看起來還不賴,我們僅用了 335ms
就處理完這個連線,那再試試看把 4 個商品 ID 加碼為 8 個商品 ID:
{{main_service}}/multi_action.php?products[]=1&products[]=5&products[]=42&products[]=55&products[]=2&products[]=77&products[]=48&products[]=53
發現問題了嗎?隨著商品數量的提升,伺服器的處理時間也呈現線性增長。
這是因為我們的程式碼使用了阻塞式連線處理方法。在這種情境中,每當我們對某個商品 ID 發出 API 請求時,程式碼會停止執行並等待直到該請求完全返回響應。換句話說,這段程式碼是逐一處理每個 API 請求的,而不是並行地同時處理多個請求。
這意味著,當我們從 4 個商品 ID 增加到 8 個商品 ID 時,所需的時間近乎是原來的兩倍,因為每個請求都在等待前一個請求完成後才開始。這種阻塞式的行為在面對大量的資料或高流量時,會使系統效能大大降低。
要解決這個問題,我們需要使用非同步的方法來處理 API 請求。這樣,我們可以同時發出多個請求,並在所有請求都返回響應後再繼續處理。這種方法可以在面對大量的請求時,顯著減少總的等待時間。
Anser 提供了 ConcurrentAction
類別,這個類別對並行連線 (Concurrent Connection)所需的方法進行了封裝,開發人員可以透過實體化 ConcurrentAction 類別後,將所有需要同時發出連線請求的 Action 加入至這個類別中。
來讓我們舉個例子,我們改寫一下上一個範例:
<?php
require_once './init.php';
use SDPMlab\Anser\Service\Action;
use Psr\Http\Message\ResponseInterface;
use SDPMlab\Anser\Service\ConcurrentAction;
header("Content-Type: application/json");
$productIds = $_GET['products'] ?? [];
if (count($productIds) == 0) {
echo json_encode([
"data" => [],
"message" => "No products, please add products id in query string. like: ?products[]=1&products[]=2"
]) . PHP_EOL;
http_response_code(200);
exit;
}
$actions = [];
foreach ($productIds as $productId) {
$actions[$productId] = (new Action(
serviceName: "ProductionService",
method: "GET",
path: "/api/v1/products/{$productId}"
))->doneHandler(static function(
ResponseInterface $response,
Action $runtimeAction
) {
$body = $response->getBody()->getContents();
$data = json_decode($body, true);
$runtimeAction->setMeaningData($data['data']);
});
}
$concurrentAction = new ConcurrentAction();
$concurrentAction->setActions(actionList: $actions);
//上述程式碼等同於
// $concurrentAction = new ConcurrentAction();
// foreach ($productIds as $productId) {
// $action = (new Action(
// serviceName: "ProductionService",
// method: "GET",
// path: "/api/v1/products/{$productId}"
// ))->doneHandler(static function(
// ResponseInterface $response,
// Action $runtimeAction
// ) {
// $body = $response->getBody()->getContents();
// $data = json_decode($body, true);
// $runtimeAction->setMeaningData($data['data']);
// });
// $concurrentAction->addAction(alias: $productId, action: $action);
// }
$concurrentAction->send();
$data = $concurrentAction->getActionsMeaningData();
echo json_encode([
"data" => $data
]) . PHP_EOL;
在這個範例中,我們透過 ConcurrentAction
類別,優化了與多個 API 端點的同時查詢,以下進行詳細的說明:
$actions
的空陣列,隨後遍歷每個商品 ID 並為每個 ID 建立一個新的 Action
類別實體。這些 Action
實體將被保存在 $actions
陣列中,用商品 ID 作為 Key。ConcurrentAction
類別。此類別是為了同時發起多個連線請求而設計的。接著,我們使用 setActions
方法將整個 $actions
陣列加入至 ConcurrentAction
實體。此外,我們也示範了如何透過 addAction
方法單獨加入每一個 Action
實體。$concurrentAction->send();
負責發起所有的連線請求。這些請求是同時進行的,不必等待前一個請求完成。$concurrentAction->getActionsMeaningData();
取得所有的請求結果。讓我們來實際執行看看著程式碼,首先從 4 個商品 ID開始:
{{main_service}}/concurrent_action.php?products[]=1&products[]=5&products[]=42&products[]=55
感覺還不錯吧,我們僅僅使用了 164ms
就處理完了,接將商品數量加大到 8 個試試:
{{main_service}}/concurrent_action.php?products[]=1&products[]=5&products[]=42&products[]=55&products[]=2&products[]=77&products[]=48&products[]=53
有發現不同嗎?這次我們僅用了 182ms
就處理完了 8 個商品的查詢請求,在幾乎沒有顯著增長請求處理時間的同時,我們還能夠取得更多的資料。
這是因為當我們使用 ConcurrentAction
進行並行請求時,每一個請求都是獨立而且幾乎同時發出的。而傳統的連續請求方式,每一個請求都必須等待前一個請求完成之後才能開始,這樣的方式會隨著請求的增加而使得總響應時間線性增長。
這裡的關鍵點在於「並行」。並行請求不意味著所有請求都會在同一個時刻完成,但它確保所有請求都幾乎在同一時間開始。所以,即使某些請求需要較長的時間,它也不會阻止或延遲其他請求的處理,也正式因為如此,我們才可以在相對短的時間內取得所有的回應。
另一個要考慮的因素是伺服器和網路的負載。當我們發出多個並行請求時,伺服器必須能夠同時處理所有這些請求,且網路必須能夠處理相應的流量。在我們的範例中,由於伺服器能夠快速地回應我們的請求,並且沒有其他瓶頸,所以我們看到的響應時間僅略有增加。這也是為什麼在文章的一開始,我們得先調整 Production Service 的伺服器組態設定檔案,使其能夠同時處理更多任務。
透過 ConcurrentAction 類別,開發人員不僅可以提高應用程式的響應速度,還可以更好地利用伺服器資源,進而提供更好的用戶體驗。
透過這篇文章,讀者可以深入瞭解阻塞式連線和非同步的並行連線之間的差異,並學會如何透過 Anser 來優化 API 請求的處理方式。在現代的微服務架構中,面對高流量、大數據和多個連線請求是很常見的。這就需要我們的應用程式能夠快速、有效地處理這些請求,而不是被單一的請求所阻塞。
從範例中我們可以看出,儘管原始的阻塞式連線方式在小量的請求中能夠正常工作,但當請求的數量增加時,其效能會呈現出線性的下降。而透過並行連線的方式,即使請求的數量倍增,所需的時間並沒有明顯的增加,顯示出並行連線處理方式對於高流量的應用程式更加合適。